package org.andengine.util.adt.pool;

import org.andengine.BuildConfig;
import org.andengine.util.debug.Debug;

import java.util.ArrayList;
import java.util.Collections;

/**
 * (c) 2010 Nicolas Gramlich
 * (c) 2011 Zynga Inc.
 *
 * @author Valentin Milea
 * @author Nicolas Gramlich
 * @since 22:19:55 - 31.08.2010
 */
public abstract class GenericPool<T> {
    // ===========================================================
    // Constants
    // ===========================================================

    // ===========================================================
    // Fields
    // ===========================================================

    private final ArrayList<T> mAvailableItems;
    private final int mGrowth;
    private final int mAvailableItemCountMaximum;

    private int mUnrecycledItemCount;

    // ===========================================================
    // Constructors
    // ===========================================================

    public GenericPool() {
        this(0);
    }

    public GenericPool(final int pInitialSize) {
        this(pInitialSize, 1);
    }

    public GenericPool(final int pInitialSize, final int pGrowth) {
        this(pInitialSize, pGrowth, Integer.MAX_VALUE);
    }

    public GenericPool(final int pInitialSize, final int pGrowth, final int pAvailableItemsMaximum) {
        if (pGrowth <= 0) {
            throw new IllegalArgumentException("pGrowth must be greater than 0!");
        }
        if (pAvailableItemsMaximum < 0) {
            throw new IllegalArgumentException("pAvailableItemsMaximum must be at least 0!");
        }

        this.mGrowth = pGrowth;
        this.mAvailableItemCountMaximum = pAvailableItemsMaximum;
        this.mAvailableItems = new ArrayList<T>(pInitialSize);

        if (pInitialSize > 0) {
            this.batchAllocatePoolItems(pInitialSize);
        }
    }

    // ===========================================================
    // Getter & Setter
    // ===========================================================

    public synchronized int getUnrecycledItemCount() {
        return this.mUnrecycledItemCount;
    }

    public synchronized int getAvailableItemCount() {
        return this.mAvailableItems.size();
    }

    public int getAvailableItemCountMaximum() {
        return this.mAvailableItemCountMaximum;
    }

    // ===========================================================
    // Methods for/from SuperClass/Interfaces
    // ===========================================================

    protected abstract T onAllocatePoolItem();

    // ===========================================================
    // Methods
    // ===========================================================

    /**
     * @param pItem every item passes this method just before it gets recycled.
     */
    protected void onHandleRecycleItem(final T pItem) {

    }

    protected T onHandleAllocatePoolItem() {
        return this.onAllocatePoolItem();
    }

    /**
     * @param pItem every item that was just obtained from the pool, passes this method.
     */
    protected void onHandleObtainItem(final T pItem) {

    }

    public synchronized void batchAllocatePoolItems(final int pCount) {
        final ArrayList<T> availableItems = this.mAvailableItems;

        int allocationCount = this.mAvailableItemCountMaximum - availableItems.size();
        if (pCount < allocationCount) {
            allocationCount = pCount;
        }

        for (int i = allocationCount - 1; i >= 0; i--) {
            availableItems.add(this.onHandleAllocatePoolItem());
        }
    }

    public synchronized T obtainPoolItem() {
        final T item;

        if (this.mAvailableItems.size() > 0) {
            item = this.mAvailableItems.remove(this.mAvailableItems.size() - 1);
        } else {
            if (this.mGrowth == 1 || this.mAvailableItemCountMaximum == 0) {
                item = this.onHandleAllocatePoolItem();
            } else {
                this.batchAllocatePoolItems(this.mGrowth);
                item = this.mAvailableItems.remove(this.mAvailableItems.size() - 1);
            }
            if (BuildConfig.DEBUG) {
                Debug.v(this.getClass().getName() + "<" + item.getClass().getSimpleName() + "> was exhausted, with " + this.mUnrecycledItemCount + " item not yet recycled. Allocated " + this.mGrowth + " more.");
            }
        }
        this.onHandleObtainItem(item);

        this.mUnrecycledItemCount++;
        return item;
    }

    public synchronized void recyclePoolItem(final T pItem) {
        if (pItem == null) {
            throw new IllegalArgumentException("Cannot recycle null item!");
        }

        this.onHandleRecycleItem(pItem);

        if (this.mAvailableItems.size() < this.mAvailableItemCountMaximum) {
            this.mAvailableItems.add(pItem);
        }

        this.mUnrecycledItemCount--;

        if (this.mUnrecycledItemCount < 0) {
            Debug.e("More items recycled than obtained!");
        }
    }

    public synchronized void shufflePoolItems() {
        Collections.shuffle(this.mAvailableItems);
    }

    // ===========================================================
    // Inner and Anonymous Classes
    // ===========================================================
}
